3 check_downvotes_enabled,
5 collect_moderated_communities,
11 use actix_web::web::Data;
12 use lemmy_apub::{ApubLikeableType, ApubObjectType};
13 use lemmy_db_queries::{
22 use lemmy_db_schema::{
27 post_report::{PostReport, PostReportForm},
31 comment_view::CommentQueryBuilder,
32 post_report_view::{PostReportQueryBuilder, PostReportView},
33 post_view::{PostQueryBuilder, PostView},
35 use lemmy_db_views_actor::{
36 community_moderator_view::CommunityModeratorView,
37 community_view::CommunityView,
39 use lemmy_structs::{blocking, post::*};
41 apub::{make_apub_endpoint, EndpointType},
42 request::fetch_iframely_and_pictrs_data,
43 utils::{check_slurs, check_slurs_opt, is_valid_post_title},
48 use lemmy_websocket::{
49 messages::{GetPostUsersOnline, SendModRoomMessage, SendPost, SendUserRoomMessage},
53 use std::str::FromStr;
55 #[async_trait::async_trait(?Send)]
56 impl Perform for CreatePost {
57 type Response = PostResponse;
61 context: &Data<LemmyContext>,
62 websocket_id: Option<ConnectionId>,
63 ) -> Result<PostResponse, LemmyError> {
64 let data: &CreatePost = &self;
65 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
67 check_slurs(&data.name)?;
68 check_slurs_opt(&data.body)?;
70 if !is_valid_post_title(&data.name) {
71 return Err(APIError::err("invalid_post_title").into());
74 check_community_ban(user.id, data.community_id, context.pool()).await?;
76 check_optional_url(&Some(data.url.to_owned()))?;
78 // Fetch Iframely and pictrs cached image
79 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
80 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
82 let post_form = PostForm {
83 name: data.name.trim().to_owned(),
84 url: data.url.to_owned(),
85 body: data.body.to_owned(),
86 community_id: data.community_id,
94 embed_title: iframely_title,
95 embed_description: iframely_description,
96 embed_html: iframely_html,
97 thumbnail_url: pictrs_thumbnail,
104 match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
107 let err_type = if e.to_string() == "value too long for type character varying(200)" {
108 "post_title_too_long"
110 "couldnt_create_post"
113 return Err(APIError::err(err_type).into());
117 let inserted_post_id = inserted_post.id;
118 let updated_post = match blocking(context.pool(), move |conn| {
120 make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
121 Post::update_ap_id(conn, inserted_post_id, apub_id)
126 Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
129 updated_post.send_create(&user, context).await?;
131 // They like their own post by default
132 let like_form = PostLikeForm {
133 post_id: inserted_post.id,
138 let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
139 if blocking(context.pool(), like).await?.is_err() {
140 return Err(APIError::err("couldnt_like_post").into());
143 updated_post.send_like(&user, context).await?;
146 let inserted_post_id = inserted_post.id;
147 let post_view = match blocking(context.pool(), move |conn| {
148 PostView::read(conn, inserted_post_id, Some(user.id))
153 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
156 let res = PostResponse { post_view };
158 context.chat_server().do_send(SendPost {
159 op: UserOperation::CreatePost,
168 #[async_trait::async_trait(?Send)]
169 impl Perform for GetPost {
170 type Response = GetPostResponse;
174 context: &Data<LemmyContext>,
175 _websocket_id: Option<ConnectionId>,
176 ) -> Result<GetPostResponse, LemmyError> {
177 let data: &GetPost = &self;
178 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
179 let user_id = user.map(|u| u.id);
182 let post_view = match blocking(context.pool(), move |conn| {
183 PostView::read(conn, id, user_id)
188 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
192 let comments = blocking(context.pool(), move |conn| {
193 CommentQueryBuilder::create(conn)
201 let community_id = post_view.community.id;
202 let moderators = blocking(context.pool(), move |conn| {
203 CommunityModeratorView::for_community(conn, community_id)
207 // Necessary for the sidebar
208 let community_view = match blocking(context.pool(), move |conn| {
209 CommunityView::read(conn, community_id, user_id)
213 Ok(community) => community,
214 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
219 .send(GetPostUsersOnline { post_id: data.id })
234 #[async_trait::async_trait(?Send)]
235 impl Perform for GetPosts {
236 type Response = GetPostsResponse;
240 context: &Data<LemmyContext>,
241 _websocket_id: Option<ConnectionId>,
242 ) -> Result<GetPostsResponse, LemmyError> {
243 let data: &GetPosts = &self;
244 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
246 let user_id = match &user {
247 Some(user) => Some(user.id),
251 let show_nsfw = match &user {
252 Some(user) => user.show_nsfw,
256 let type_ = ListingType::from_str(&data.type_)?;
257 let sort = SortType::from_str(&data.sort)?;
259 let page = data.page;
260 let limit = data.limit;
261 let community_id = data.community_id;
262 let community_name = data.community_name.to_owned();
263 let posts = match blocking(context.pool(), move |conn| {
264 PostQueryBuilder::create(conn)
265 .listing_type(&type_)
267 .show_nsfw(show_nsfw)
268 .community_id(community_id)
269 .community_name(community_name)
278 Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
281 Ok(GetPostsResponse { posts })
285 #[async_trait::async_trait(?Send)]
286 impl Perform for CreatePostLike {
287 type Response = PostResponse;
291 context: &Data<LemmyContext>,
292 websocket_id: Option<ConnectionId>,
293 ) -> Result<PostResponse, LemmyError> {
294 let data: &CreatePostLike = &self;
295 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
297 // Don't do a downvote if site has downvotes disabled
298 check_downvotes_enabled(data.score, context.pool()).await?;
300 // Check for a community ban
301 let post_id = data.post_id;
302 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
304 check_community_ban(user.id, post.community_id, context.pool()).await?;
306 let like_form = PostLikeForm {
307 post_id: data.post_id,
312 // Remove any likes first
313 let user_id = user.id;
314 blocking(context.pool(), move |conn| {
315 PostLike::remove(conn, user_id, post_id)
319 // Only add the like if the score isnt 0
320 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
322 let like_form2 = like_form.clone();
323 let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
324 if blocking(context.pool(), like).await?.is_err() {
325 return Err(APIError::err("couldnt_like_post").into());
328 if like_form.score == 1 {
329 post.send_like(&user, context).await?;
330 } else if like_form.score == -1 {
331 post.send_dislike(&user, context).await?;
334 post.send_undo_like(&user, context).await?;
337 let post_id = data.post_id;
338 let user_id = user.id;
339 let post_view = match blocking(context.pool(), move |conn| {
340 PostView::read(conn, post_id, Some(user_id))
345 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
348 let res = PostResponse { post_view };
350 context.chat_server().do_send(SendPost {
351 op: UserOperation::CreatePostLike,
360 #[async_trait::async_trait(?Send)]
361 impl Perform for EditPost {
362 type Response = PostResponse;
366 context: &Data<LemmyContext>,
367 websocket_id: Option<ConnectionId>,
368 ) -> Result<PostResponse, LemmyError> {
369 let data: &EditPost = &self;
370 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
372 check_slurs(&data.name)?;
373 check_slurs_opt(&data.body)?;
375 if !is_valid_post_title(&data.name) {
376 return Err(APIError::err("invalid_post_title").into());
379 let post_id = data.post_id;
380 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
382 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
384 // Verify that only the creator can edit
385 if !Post::is_post_creator(user.id, orig_post.creator_id) {
386 return Err(APIError::err("no_post_edit_allowed").into());
389 // Fetch Iframely and Pictrs cached image
390 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
391 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
393 let post_form = PostForm {
394 name: data.name.trim().to_owned(),
395 url: data.url.to_owned(),
396 body: data.body.to_owned(),
398 creator_id: orig_post.creator_id.to_owned(),
399 community_id: orig_post.community_id,
400 removed: Some(orig_post.removed),
401 deleted: Some(orig_post.deleted),
402 locked: Some(orig_post.locked),
403 stickied: Some(orig_post.stickied),
404 updated: Some(naive_now()),
405 embed_title: iframely_title,
406 embed_description: iframely_description,
407 embed_html: iframely_html,
408 thumbnail_url: pictrs_thumbnail,
409 ap_id: Some(orig_post.ap_id),
410 local: orig_post.local,
414 let post_id = data.post_id;
415 let res = blocking(context.pool(), move |conn| {
416 Post::update(conn, post_id, &post_form)
419 let updated_post: Post = match res {
422 let err_type = if e.to_string() == "value too long for type character varying(200)" {
423 "post_title_too_long"
425 "couldnt_update_post"
428 return Err(APIError::err(err_type).into());
433 updated_post.send_update(&user, context).await?;
435 let post_id = data.post_id;
436 let post_view = blocking(context.pool(), move |conn| {
437 PostView::read(conn, post_id, Some(user.id))
441 let res = PostResponse { post_view };
443 context.chat_server().do_send(SendPost {
444 op: UserOperation::EditPost,
453 #[async_trait::async_trait(?Send)]
454 impl Perform for DeletePost {
455 type Response = PostResponse;
459 context: &Data<LemmyContext>,
460 websocket_id: Option<ConnectionId>,
461 ) -> Result<PostResponse, LemmyError> {
462 let data: &DeletePost = &self;
463 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
465 let post_id = data.post_id;
466 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
468 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
470 // Verify that only the creator can delete
471 if !Post::is_post_creator(user.id, orig_post.creator_id) {
472 return Err(APIError::err("no_post_edit_allowed").into());
476 let post_id = data.post_id;
477 let deleted = data.deleted;
478 let updated_post = blocking(context.pool(), move |conn| {
479 Post::update_deleted(conn, post_id, deleted)
485 updated_post.send_delete(&user, context).await?;
487 updated_post.send_undo_delete(&user, context).await?;
491 let post_id = data.post_id;
492 let post_view = blocking(context.pool(), move |conn| {
493 PostView::read(conn, post_id, Some(user.id))
497 let res = PostResponse { post_view };
499 context.chat_server().do_send(SendPost {
500 op: UserOperation::DeletePost,
509 #[async_trait::async_trait(?Send)]
510 impl Perform for RemovePost {
511 type Response = PostResponse;
515 context: &Data<LemmyContext>,
516 websocket_id: Option<ConnectionId>,
517 ) -> Result<PostResponse, LemmyError> {
518 let data: &RemovePost = &self;
519 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
521 let post_id = data.post_id;
522 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
524 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
526 // Verify that only the mods can remove
527 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
530 let post_id = data.post_id;
531 let removed = data.removed;
532 let updated_post = blocking(context.pool(), move |conn| {
533 Post::update_removed(conn, post_id, removed)
538 let form = ModRemovePostForm {
539 mod_user_id: user.id,
540 post_id: data.post_id,
541 removed: Some(removed),
542 reason: data.reason.to_owned(),
544 blocking(context.pool(), move |conn| {
545 ModRemovePost::create(conn, &form)
551 updated_post.send_remove(&user, context).await?;
553 updated_post.send_undo_remove(&user, context).await?;
557 let post_id = data.post_id;
558 let user_id = user.id;
559 let post_view = blocking(context.pool(), move |conn| {
560 PostView::read(conn, post_id, Some(user_id))
564 let res = PostResponse { post_view };
566 context.chat_server().do_send(SendPost {
567 op: UserOperation::RemovePost,
576 #[async_trait::async_trait(?Send)]
577 impl Perform for LockPost {
578 type Response = PostResponse;
582 context: &Data<LemmyContext>,
583 websocket_id: Option<ConnectionId>,
584 ) -> Result<PostResponse, LemmyError> {
585 let data: &LockPost = &self;
586 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
588 let post_id = data.post_id;
589 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
591 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
593 // Verify that only the mods can lock
594 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
597 let post_id = data.post_id;
598 let locked = data.locked;
599 let updated_post = blocking(context.pool(), move |conn| {
600 Post::update_locked(conn, post_id, locked)
605 let form = ModLockPostForm {
606 mod_user_id: user.id,
607 post_id: data.post_id,
608 locked: Some(locked),
610 blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
613 updated_post.send_update(&user, context).await?;
616 let post_id = data.post_id;
617 let post_view = blocking(context.pool(), move |conn| {
618 PostView::read(conn, post_id, Some(user.id))
622 let res = PostResponse { post_view };
624 context.chat_server().do_send(SendPost {
625 op: UserOperation::LockPost,
634 #[async_trait::async_trait(?Send)]
635 impl Perform for StickyPost {
636 type Response = PostResponse;
640 context: &Data<LemmyContext>,
641 websocket_id: Option<ConnectionId>,
642 ) -> Result<PostResponse, LemmyError> {
643 let data: &StickyPost = &self;
644 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
646 let post_id = data.post_id;
647 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
649 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
651 // Verify that only the mods can sticky
652 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
655 let post_id = data.post_id;
656 let stickied = data.stickied;
657 let updated_post = blocking(context.pool(), move |conn| {
658 Post::update_stickied(conn, post_id, stickied)
663 let form = ModStickyPostForm {
664 mod_user_id: user.id,
665 post_id: data.post_id,
666 stickied: Some(stickied),
668 blocking(context.pool(), move |conn| {
669 ModStickyPost::create(conn, &form)
674 // TODO stickied should pry work like locked for ease of use
675 updated_post.send_update(&user, context).await?;
678 let post_id = data.post_id;
679 let post_view = blocking(context.pool(), move |conn| {
680 PostView::read(conn, post_id, Some(user.id))
684 let res = PostResponse { post_view };
686 context.chat_server().do_send(SendPost {
687 op: UserOperation::StickyPost,
696 #[async_trait::async_trait(?Send)]
697 impl Perform for SavePost {
698 type Response = PostResponse;
702 context: &Data<LemmyContext>,
703 _websocket_id: Option<ConnectionId>,
704 ) -> Result<PostResponse, LemmyError> {
705 let data: &SavePost = &self;
706 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
708 let post_saved_form = PostSavedForm {
709 post_id: data.post_id,
714 let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
715 if blocking(context.pool(), save).await?.is_err() {
716 return Err(APIError::err("couldnt_save_post").into());
719 let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
720 if blocking(context.pool(), unsave).await?.is_err() {
721 return Err(APIError::err("couldnt_save_post").into());
725 let post_id = data.post_id;
726 let user_id = user.id;
727 let post_view = blocking(context.pool(), move |conn| {
728 PostView::read(conn, post_id, Some(user_id))
732 Ok(PostResponse { post_view })
736 /// Creates a post report and notifies the moderators of the community
737 #[async_trait::async_trait(?Send)]
738 impl Perform for CreatePostReport {
739 type Response = CreatePostReportResponse;
743 context: &Data<LemmyContext>,
744 websocket_id: Option<ConnectionId>,
745 ) -> Result<CreatePostReportResponse, LemmyError> {
746 let data: &CreatePostReport = &self;
747 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
749 // check size of report and check for whitespace
750 let reason = data.reason.trim();
751 if reason.is_empty() {
752 return Err(APIError::err("report_reason_required").into());
754 if reason.len() > 1000 {
755 return Err(APIError::err("report_too_long").into());
758 let user_id = user.id;
759 let post_id = data.post_id;
760 let post_view = blocking(context.pool(), move |conn| {
761 PostView::read(&conn, post_id, None)
765 check_community_ban(user_id, post_view.community.id, context.pool()).await?;
767 let report_form = PostReportForm {
770 original_post_name: post_view.post.name,
771 original_post_url: post_view.post.url,
772 original_post_body: post_view.post.body,
773 reason: data.reason.to_owned(),
776 let report = match blocking(context.pool(), move |conn| {
777 PostReport::report(conn, &report_form)
781 Ok(report) => report,
782 Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
785 let res = CreatePostReportResponse { success: true };
787 context.chat_server().do_send(SendUserRoomMessage {
788 op: UserOperation::CreatePostReport,
789 response: res.clone(),
790 recipient_id: user.id,
794 context.chat_server().do_send(SendModRoomMessage {
795 op: UserOperation::CreatePostReport,
797 community_id: post_view.community.id,
805 /// Resolves or unresolves a post report and notifies the moderators of the community
806 #[async_trait::async_trait(?Send)]
807 impl Perform for ResolvePostReport {
808 type Response = ResolvePostReportResponse;
812 context: &Data<LemmyContext>,
813 websocket_id: Option<ConnectionId>,
814 ) -> Result<ResolvePostReportResponse, LemmyError> {
815 let data: &ResolvePostReport = &self;
816 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
818 let report_id = data.report_id;
819 let report = blocking(context.pool(), move |conn| {
820 PostReportView::read(&conn, report_id)
824 let user_id = user.id;
825 is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
827 let resolved = data.resolved;
828 let resolve_fun = move |conn: &'_ _| {
830 PostReport::resolve(conn, report_id, user_id)
832 PostReport::unresolve(conn, report_id, user_id)
836 let res = ResolvePostReportResponse {
841 if blocking(context.pool(), resolve_fun).await?.is_err() {
842 return Err(APIError::err("couldnt_resolve_report").into());
845 context.chat_server().do_send(SendModRoomMessage {
846 op: UserOperation::ResolvePostReport,
847 response: res.clone(),
848 community_id: report.community.id,
856 /// Lists post reports for a community if an id is supplied
857 /// or returns all post reports for communities a user moderates
858 #[async_trait::async_trait(?Send)]
859 impl Perform for ListPostReports {
860 type Response = ListPostReportsResponse;
864 context: &Data<LemmyContext>,
865 websocket_id: Option<ConnectionId>,
866 ) -> Result<ListPostReportsResponse, LemmyError> {
867 let data: &ListPostReports = &self;
868 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
870 let user_id = user.id;
871 let community_id = data.community;
873 collect_moderated_communities(user_id, community_id, context.pool()).await?;
875 let page = data.page;
876 let limit = data.limit;
877 let posts = blocking(context.pool(), move |conn| {
878 PostReportQueryBuilder::create(conn)
879 .community_ids(community_ids)
886 let res = ListPostReportsResponse { posts };
888 context.chat_server().do_send(SendUserRoomMessage {
889 op: UserOperation::ListPostReports,
890 response: res.clone(),
891 recipient_id: user.id,