3 check_downvotes_enabled,
5 collect_moderated_communities,
11 use actix_web::web::Data;
12 use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
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 request::fetch_iframely_and_pictrs_data,
42 utils::{check_slurs, check_slurs_opt, is_valid_post_title},
47 use lemmy_websocket::{
48 messages::{GetPostUsersOnline, SendModRoomMessage, SendPost, SendUserRoomMessage},
52 use std::str::FromStr;
54 #[async_trait::async_trait(?Send)]
55 impl Perform for CreatePost {
56 type Response = PostResponse;
60 context: &Data<LemmyContext>,
61 websocket_id: Option<ConnectionId>,
62 ) -> Result<PostResponse, LemmyError> {
63 let data: &CreatePost = &self;
64 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
66 check_slurs(&data.name)?;
67 check_slurs_opt(&data.body)?;
69 if !is_valid_post_title(&data.name) {
70 return Err(ApiError::err("invalid_post_title").into());
73 check_community_ban(user.id, data.community_id, context.pool()).await?;
75 check_optional_url(&Some(data.url.to_owned()))?;
77 // Fetch Iframely and pictrs cached image
78 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
79 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
81 let post_form = PostForm {
82 name: data.name.trim().to_owned(),
83 url: data.url.to_owned(),
84 body: data.body.to_owned(),
85 community_id: data.community_id,
93 embed_title: iframely_title,
94 embed_description: iframely_description,
95 embed_html: iframely_html,
96 thumbnail_url: pictrs_thumbnail,
103 match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
106 let err_type = if e.to_string() == "value too long for type character varying(200)" {
107 "post_title_too_long"
109 "couldnt_create_post"
112 return Err(ApiError::err(err_type).into());
116 let inserted_post_id = inserted_post.id;
117 let updated_post = match blocking(context.pool(), move |conn| -> Result<Post, LemmyError> {
118 let apub_id = generate_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string())?;
119 Ok(Post::update_ap_id(conn, inserted_post_id, apub_id)?)
124 Err(_e) => return Err(ApiError::err("couldnt_create_post").into()),
127 updated_post.send_create(&user, context).await?;
129 // They like their own post by default
130 let like_form = PostLikeForm {
131 post_id: inserted_post.id,
136 let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
137 if blocking(context.pool(), like).await?.is_err() {
138 return Err(ApiError::err("couldnt_like_post").into());
141 updated_post.send_like(&user, context).await?;
144 let inserted_post_id = inserted_post.id;
145 let post_view = match blocking(context.pool(), move |conn| {
146 PostView::read(conn, inserted_post_id, Some(user.id))
151 Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
154 let res = PostResponse { post_view };
156 context.chat_server().do_send(SendPost {
157 op: UserOperation::CreatePost,
166 #[async_trait::async_trait(?Send)]
167 impl Perform for GetPost {
168 type Response = GetPostResponse;
172 context: &Data<LemmyContext>,
173 _websocket_id: Option<ConnectionId>,
174 ) -> Result<GetPostResponse, LemmyError> {
175 let data: &GetPost = &self;
176 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
177 let user_id = user.map(|u| u.id);
180 let post_view = match blocking(context.pool(), move |conn| {
181 PostView::read(conn, id, user_id)
186 Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
190 let comments = blocking(context.pool(), move |conn| {
191 CommentQueryBuilder::create(conn)
199 let community_id = post_view.community.id;
200 let moderators = blocking(context.pool(), move |conn| {
201 CommunityModeratorView::for_community(conn, community_id)
205 // Necessary for the sidebar
206 let community_view = match blocking(context.pool(), move |conn| {
207 CommunityView::read(conn, community_id, user_id)
211 Ok(community) => community,
212 Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
217 .send(GetPostUsersOnline { post_id: data.id })
232 #[async_trait::async_trait(?Send)]
233 impl Perform for GetPosts {
234 type Response = GetPostsResponse;
238 context: &Data<LemmyContext>,
239 _websocket_id: Option<ConnectionId>,
240 ) -> Result<GetPostsResponse, LemmyError> {
241 let data: &GetPosts = &self;
242 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
244 let user_id = match &user {
245 Some(user) => Some(user.id),
249 let show_nsfw = match &user {
250 Some(user) => user.show_nsfw,
254 let type_ = ListingType::from_str(&data.type_)?;
255 let sort = SortType::from_str(&data.sort)?;
257 let page = data.page;
258 let limit = data.limit;
259 let community_id = data.community_id;
260 let community_name = data.community_name.to_owned();
261 let posts = match blocking(context.pool(), move |conn| {
262 PostQueryBuilder::create(conn)
263 .listing_type(&type_)
265 .show_nsfw(show_nsfw)
266 .community_id(community_id)
267 .community_name(community_name)
276 Err(_e) => return Err(ApiError::err("couldnt_get_posts").into()),
279 Ok(GetPostsResponse { posts })
283 #[async_trait::async_trait(?Send)]
284 impl Perform for CreatePostLike {
285 type Response = PostResponse;
289 context: &Data<LemmyContext>,
290 websocket_id: Option<ConnectionId>,
291 ) -> Result<PostResponse, LemmyError> {
292 let data: &CreatePostLike = &self;
293 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
295 // Don't do a downvote if site has downvotes disabled
296 check_downvotes_enabled(data.score, context.pool()).await?;
298 // Check for a community ban
299 let post_id = data.post_id;
300 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
302 check_community_ban(user.id, post.community_id, context.pool()).await?;
304 let like_form = PostLikeForm {
305 post_id: data.post_id,
310 // Remove any likes first
311 let user_id = user.id;
312 blocking(context.pool(), move |conn| {
313 PostLike::remove(conn, user_id, post_id)
317 // Only add the like if the score isnt 0
318 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
320 let like_form2 = like_form.clone();
321 let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
322 if blocking(context.pool(), like).await?.is_err() {
323 return Err(ApiError::err("couldnt_like_post").into());
326 if like_form.score == 1 {
327 post.send_like(&user, context).await?;
328 } else if like_form.score == -1 {
329 post.send_dislike(&user, context).await?;
332 post.send_undo_like(&user, context).await?;
335 let post_id = data.post_id;
336 let user_id = user.id;
337 let post_view = match blocking(context.pool(), move |conn| {
338 PostView::read(conn, post_id, Some(user_id))
343 Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
346 let res = PostResponse { post_view };
348 context.chat_server().do_send(SendPost {
349 op: UserOperation::CreatePostLike,
358 #[async_trait::async_trait(?Send)]
359 impl Perform for EditPost {
360 type Response = PostResponse;
364 context: &Data<LemmyContext>,
365 websocket_id: Option<ConnectionId>,
366 ) -> Result<PostResponse, LemmyError> {
367 let data: &EditPost = &self;
368 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
370 check_slurs(&data.name)?;
371 check_slurs_opt(&data.body)?;
373 if !is_valid_post_title(&data.name) {
374 return Err(ApiError::err("invalid_post_title").into());
377 let post_id = data.post_id;
378 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
380 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
382 // Verify that only the creator can edit
383 if !Post::is_post_creator(user.id, orig_post.creator_id) {
384 return Err(ApiError::err("no_post_edit_allowed").into());
387 // Fetch Iframely and Pictrs cached image
388 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
389 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
391 let post_form = PostForm {
392 name: data.name.trim().to_owned(),
393 url: data.url.to_owned(),
394 body: data.body.to_owned(),
396 creator_id: orig_post.creator_id.to_owned(),
397 community_id: orig_post.community_id,
398 removed: Some(orig_post.removed),
399 deleted: Some(orig_post.deleted),
400 locked: Some(orig_post.locked),
401 stickied: Some(orig_post.stickied),
402 updated: Some(naive_now()),
403 embed_title: iframely_title,
404 embed_description: iframely_description,
405 embed_html: iframely_html,
406 thumbnail_url: pictrs_thumbnail,
407 ap_id: Some(orig_post.ap_id),
408 local: orig_post.local,
412 let post_id = data.post_id;
413 let res = blocking(context.pool(), move |conn| {
414 Post::update(conn, post_id, &post_form)
417 let updated_post: Post = match res {
420 let err_type = if e.to_string() == "value too long for type character varying(200)" {
421 "post_title_too_long"
423 "couldnt_update_post"
426 return Err(ApiError::err(err_type).into());
431 updated_post.send_update(&user, context).await?;
433 let post_id = data.post_id;
434 let post_view = blocking(context.pool(), move |conn| {
435 PostView::read(conn, post_id, Some(user.id))
439 let res = PostResponse { post_view };
441 context.chat_server().do_send(SendPost {
442 op: UserOperation::EditPost,
451 #[async_trait::async_trait(?Send)]
452 impl Perform for DeletePost {
453 type Response = PostResponse;
457 context: &Data<LemmyContext>,
458 websocket_id: Option<ConnectionId>,
459 ) -> Result<PostResponse, LemmyError> {
460 let data: &DeletePost = &self;
461 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
463 let post_id = data.post_id;
464 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
466 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
468 // Verify that only the creator can delete
469 if !Post::is_post_creator(user.id, orig_post.creator_id) {
470 return Err(ApiError::err("no_post_edit_allowed").into());
474 let post_id = data.post_id;
475 let deleted = data.deleted;
476 let updated_post = blocking(context.pool(), move |conn| {
477 Post::update_deleted(conn, post_id, deleted)
483 updated_post.send_delete(&user, context).await?;
485 updated_post.send_undo_delete(&user, context).await?;
489 let post_id = data.post_id;
490 let post_view = blocking(context.pool(), move |conn| {
491 PostView::read(conn, post_id, Some(user.id))
495 let res = PostResponse { post_view };
497 context.chat_server().do_send(SendPost {
498 op: UserOperation::DeletePost,
507 #[async_trait::async_trait(?Send)]
508 impl Perform for RemovePost {
509 type Response = PostResponse;
513 context: &Data<LemmyContext>,
514 websocket_id: Option<ConnectionId>,
515 ) -> Result<PostResponse, LemmyError> {
516 let data: &RemovePost = &self;
517 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
519 let post_id = data.post_id;
520 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
522 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
524 // Verify that only the mods can remove
525 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
528 let post_id = data.post_id;
529 let removed = data.removed;
530 let updated_post = blocking(context.pool(), move |conn| {
531 Post::update_removed(conn, post_id, removed)
536 let form = ModRemovePostForm {
537 mod_person_id: user.id,
538 post_id: data.post_id,
539 removed: Some(removed),
540 reason: data.reason.to_owned(),
542 blocking(context.pool(), move |conn| {
543 ModRemovePost::create(conn, &form)
549 updated_post.send_remove(&user, context).await?;
551 updated_post.send_undo_remove(&user, context).await?;
555 let post_id = data.post_id;
556 let user_id = user.id;
557 let post_view = blocking(context.pool(), move |conn| {
558 PostView::read(conn, post_id, Some(user_id))
562 let res = PostResponse { post_view };
564 context.chat_server().do_send(SendPost {
565 op: UserOperation::RemovePost,
574 #[async_trait::async_trait(?Send)]
575 impl Perform for LockPost {
576 type Response = PostResponse;
580 context: &Data<LemmyContext>,
581 websocket_id: Option<ConnectionId>,
582 ) -> Result<PostResponse, LemmyError> {
583 let data: &LockPost = &self;
584 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
586 let post_id = data.post_id;
587 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
589 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
591 // Verify that only the mods can lock
592 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
595 let post_id = data.post_id;
596 let locked = data.locked;
597 let updated_post = blocking(context.pool(), move |conn| {
598 Post::update_locked(conn, post_id, locked)
603 let form = ModLockPostForm {
604 mod_person_id: user.id,
605 post_id: data.post_id,
606 locked: Some(locked),
608 blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
611 updated_post.send_update(&user, context).await?;
614 let post_id = data.post_id;
615 let post_view = blocking(context.pool(), move |conn| {
616 PostView::read(conn, post_id, Some(user.id))
620 let res = PostResponse { post_view };
622 context.chat_server().do_send(SendPost {
623 op: UserOperation::LockPost,
632 #[async_trait::async_trait(?Send)]
633 impl Perform for StickyPost {
634 type Response = PostResponse;
638 context: &Data<LemmyContext>,
639 websocket_id: Option<ConnectionId>,
640 ) -> Result<PostResponse, LemmyError> {
641 let data: &StickyPost = &self;
642 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
644 let post_id = data.post_id;
645 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
647 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
649 // Verify that only the mods can sticky
650 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
653 let post_id = data.post_id;
654 let stickied = data.stickied;
655 let updated_post = blocking(context.pool(), move |conn| {
656 Post::update_stickied(conn, post_id, stickied)
661 let form = ModStickyPostForm {
662 mod_person_id: user.id,
663 post_id: data.post_id,
664 stickied: Some(stickied),
666 blocking(context.pool(), move |conn| {
667 ModStickyPost::create(conn, &form)
672 // TODO stickied should pry work like locked for ease of use
673 updated_post.send_update(&user, context).await?;
676 let post_id = data.post_id;
677 let post_view = blocking(context.pool(), move |conn| {
678 PostView::read(conn, post_id, Some(user.id))
682 let res = PostResponse { post_view };
684 context.chat_server().do_send(SendPost {
685 op: UserOperation::StickyPost,
694 #[async_trait::async_trait(?Send)]
695 impl Perform for SavePost {
696 type Response = PostResponse;
700 context: &Data<LemmyContext>,
701 _websocket_id: Option<ConnectionId>,
702 ) -> Result<PostResponse, LemmyError> {
703 let data: &SavePost = &self;
704 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
706 let post_saved_form = PostSavedForm {
707 post_id: data.post_id,
712 let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
713 if blocking(context.pool(), save).await?.is_err() {
714 return Err(ApiError::err("couldnt_save_post").into());
717 let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
718 if blocking(context.pool(), unsave).await?.is_err() {
719 return Err(ApiError::err("couldnt_save_post").into());
723 let post_id = data.post_id;
724 let user_id = user.id;
725 let post_view = blocking(context.pool(), move |conn| {
726 PostView::read(conn, post_id, Some(user_id))
730 Ok(PostResponse { post_view })
734 /// Creates a post report and notifies the moderators of the community
735 #[async_trait::async_trait(?Send)]
736 impl Perform for CreatePostReport {
737 type Response = CreatePostReportResponse;
741 context: &Data<LemmyContext>,
742 websocket_id: Option<ConnectionId>,
743 ) -> Result<CreatePostReportResponse, LemmyError> {
744 let data: &CreatePostReport = &self;
745 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
747 // check size of report and check for whitespace
748 let reason = data.reason.trim();
749 if reason.is_empty() {
750 return Err(ApiError::err("report_reason_required").into());
752 if reason.chars().count() > 1000 {
753 return Err(ApiError::err("report_too_long").into());
756 let user_id = user.id;
757 let post_id = data.post_id;
758 let post_view = blocking(context.pool(), move |conn| {
759 PostView::read(&conn, post_id, None)
763 check_community_ban(user_id, post_view.community.id, context.pool()).await?;
765 let report_form = PostReportForm {
768 original_post_name: post_view.post.name,
769 original_post_url: post_view.post.url,
770 original_post_body: post_view.post.body,
771 reason: data.reason.to_owned(),
774 let report = match blocking(context.pool(), move |conn| {
775 PostReport::report(conn, &report_form)
779 Ok(report) => report,
780 Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
783 let res = CreatePostReportResponse { success: true };
785 context.chat_server().do_send(SendUserRoomMessage {
786 op: UserOperation::CreatePostReport,
787 response: res.clone(),
788 recipient_id: user.id,
792 context.chat_server().do_send(SendModRoomMessage {
793 op: UserOperation::CreatePostReport,
795 community_id: post_view.community.id,
803 /// Resolves or unresolves a post report and notifies the moderators of the community
804 #[async_trait::async_trait(?Send)]
805 impl Perform for ResolvePostReport {
806 type Response = ResolvePostReportResponse;
810 context: &Data<LemmyContext>,
811 websocket_id: Option<ConnectionId>,
812 ) -> Result<ResolvePostReportResponse, LemmyError> {
813 let data: &ResolvePostReport = &self;
814 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
816 let report_id = data.report_id;
817 let report = blocking(context.pool(), move |conn| {
818 PostReportView::read(&conn, report_id)
822 let user_id = user.id;
823 is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
825 let resolved = data.resolved;
826 let resolve_fun = move |conn: &'_ _| {
828 PostReport::resolve(conn, report_id, user_id)
830 PostReport::unresolve(conn, report_id, user_id)
834 let res = ResolvePostReportResponse {
839 if blocking(context.pool(), resolve_fun).await?.is_err() {
840 return Err(ApiError::err("couldnt_resolve_report").into());
843 context.chat_server().do_send(SendModRoomMessage {
844 op: UserOperation::ResolvePostReport,
845 response: res.clone(),
846 community_id: report.community.id,
854 /// Lists post reports for a community if an id is supplied
855 /// or returns all post reports for communities a user moderates
856 #[async_trait::async_trait(?Send)]
857 impl Perform for ListPostReports {
858 type Response = ListPostReportsResponse;
862 context: &Data<LemmyContext>,
863 websocket_id: Option<ConnectionId>,
864 ) -> Result<ListPostReportsResponse, LemmyError> {
865 let data: &ListPostReports = &self;
866 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
868 let user_id = user.id;
869 let community_id = data.community;
871 collect_moderated_communities(user_id, community_id, context.pool()).await?;
873 let page = data.page;
874 let limit = data.limit;
875 let posts = blocking(context.pool(), move |conn| {
876 PostReportQueryBuilder::create(conn)
877 .community_ids(community_ids)
884 let res = ListPostReportsResponse { posts };
886 context.chat_server().do_send(SendUserRoomMessage {
887 op: UserOperation::ListPostReports,
888 response: res.clone(),
889 recipient_id: user.id,