3 check_downvotes_enabled,
4 collect_moderated_communities,
5 get_local_user_view_from_jwt,
6 get_local_user_view_from_jwt_opt,
10 use actix_web::web::Data;
11 use lemmy_api_structs::{blocking, post::*};
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,
40 request::fetch_iframely_and_pictrs_data,
41 utils::{check_slurs, check_slurs_opt, is_valid_post_title},
46 use lemmy_websocket::{
47 messages::{GetPostUsersOnline, SendModRoomMessage, SendPost, SendUserRoomMessage},
51 use std::str::FromStr;
53 #[async_trait::async_trait(?Send)]
54 impl Perform for CreatePost {
55 type Response = PostResponse;
59 context: &Data<LemmyContext>,
60 websocket_id: Option<ConnectionId>,
61 ) -> Result<PostResponse, LemmyError> {
62 let data: &CreatePost = &self;
63 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
65 check_slurs(&data.name)?;
66 check_slurs_opt(&data.body)?;
68 if !is_valid_post_title(&data.name) {
69 return Err(ApiError::err("invalid_post_title").into());
72 check_community_ban(local_user_view.person.id, data.community_id, context.pool()).await?;
74 // Fetch Iframely and pictrs cached image
75 let data_url = data.url.as_ref();
76 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
77 fetch_iframely_and_pictrs_data(context.client(), data_url).await;
79 let post_form = PostForm {
80 name: data.name.trim().to_owned(),
81 url: data_url.map(|u| u.to_owned().into()),
82 body: data.body.to_owned(),
83 community_id: data.community_id,
84 creator_id: local_user_view.person.id,
91 embed_title: iframely_title,
92 embed_description: iframely_description,
93 embed_html: iframely_html,
94 thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
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| -> Result<Post, LemmyError> {
116 let apub_id = generate_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string())?;
117 Ok(Post::update_ap_id(conn, inserted_post_id, apub_id)?)
122 Err(_e) => return Err(ApiError::err("couldnt_create_post").into()),
126 .send_create(&local_user_view.person, context)
129 // They like their own post by default
130 let like_form = PostLikeForm {
131 post_id: inserted_post.id,
132 person_id: local_user_view.person.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());
142 .send_like(&local_user_view.person, context)
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(local_user_view.person.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 local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
179 let person_id = local_user_view.map(|u| u.person.id);
182 let post_view = match blocking(context.pool(), move |conn| {
183 PostView::read(conn, id, person_id)
188 Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
192 let comments = blocking(context.pool(), move |conn| {
193 CommentQueryBuilder::create(conn)
194 .my_person_id(person_id)
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, person_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 local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
246 let person_id = match &local_user_view {
247 Some(uv) => Some(uv.person.id),
251 let show_nsfw = match &local_user_view {
252 Some(uv) => uv.local_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 saved_only = data.saved_only;
265 let posts = match blocking(context.pool(), move |conn| {
266 PostQueryBuilder::create(conn)
267 .listing_type(&type_)
269 .show_nsfw(show_nsfw)
270 .community_id(community_id)
271 .community_name(community_name)
272 .saved_only(saved_only)
273 .my_person_id(person_id)
281 Err(_e) => return Err(ApiError::err("couldnt_get_posts").into()),
284 Ok(GetPostsResponse { posts })
288 #[async_trait::async_trait(?Send)]
289 impl Perform for CreatePostLike {
290 type Response = PostResponse;
294 context: &Data<LemmyContext>,
295 websocket_id: Option<ConnectionId>,
296 ) -> Result<PostResponse, LemmyError> {
297 let data: &CreatePostLike = &self;
298 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
300 // Don't do a downvote if site has downvotes disabled
301 check_downvotes_enabled(data.score, context.pool()).await?;
303 // Check for a community ban
304 let post_id = data.post_id;
305 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
307 check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
309 let like_form = PostLikeForm {
310 post_id: data.post_id,
311 person_id: local_user_view.person.id,
315 // Remove any likes first
316 let person_id = local_user_view.person.id;
317 blocking(context.pool(), move |conn| {
318 PostLike::remove(conn, person_id, post_id)
322 // Only add the like if the score isnt 0
323 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
325 let like_form2 = like_form.clone();
326 let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
327 if blocking(context.pool(), like).await?.is_err() {
328 return Err(ApiError::err("couldnt_like_post").into());
331 if like_form.score == 1 {
332 post.send_like(&local_user_view.person, context).await?;
333 } else if like_form.score == -1 {
334 post.send_dislike(&local_user_view.person, context).await?;
338 .send_undo_like(&local_user_view.person, context)
342 let post_id = data.post_id;
343 let person_id = local_user_view.person.id;
344 let post_view = match blocking(context.pool(), move |conn| {
345 PostView::read(conn, post_id, Some(person_id))
350 Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
353 let res = PostResponse { post_view };
355 context.chat_server().do_send(SendPost {
356 op: UserOperation::CreatePostLike,
365 #[async_trait::async_trait(?Send)]
366 impl Perform for EditPost {
367 type Response = PostResponse;
371 context: &Data<LemmyContext>,
372 websocket_id: Option<ConnectionId>,
373 ) -> Result<PostResponse, LemmyError> {
374 let data: &EditPost = &self;
375 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
377 check_slurs(&data.name)?;
378 check_slurs_opt(&data.body)?;
380 if !is_valid_post_title(&data.name) {
381 return Err(ApiError::err("invalid_post_title").into());
384 let post_id = data.post_id;
385 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
388 local_user_view.person.id,
389 orig_post.community_id,
394 // Verify that only the creator can edit
395 if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
396 return Err(ApiError::err("no_post_edit_allowed").into());
399 // Fetch Iframely and Pictrs cached image
400 let data_url = data.url.as_ref();
401 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
402 fetch_iframely_and_pictrs_data(context.client(), data_url).await;
404 let post_form = PostForm {
405 name: data.name.trim().to_owned(),
406 url: data_url.map(|u| u.to_owned().into()),
407 body: data.body.to_owned(),
409 creator_id: orig_post.creator_id.to_owned(),
410 community_id: orig_post.community_id,
411 removed: Some(orig_post.removed),
412 deleted: Some(orig_post.deleted),
413 locked: Some(orig_post.locked),
414 stickied: Some(orig_post.stickied),
415 updated: Some(naive_now()),
416 embed_title: iframely_title,
417 embed_description: iframely_description,
418 embed_html: iframely_html,
419 thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
420 ap_id: Some(orig_post.ap_id),
421 local: orig_post.local,
425 let post_id = data.post_id;
426 let res = blocking(context.pool(), move |conn| {
427 Post::update(conn, post_id, &post_form)
430 let updated_post: Post = match res {
433 let err_type = if e.to_string() == "value too long for type character varying(200)" {
434 "post_title_too_long"
436 "couldnt_update_post"
439 return Err(ApiError::err(err_type).into());
445 .send_update(&local_user_view.person, context)
448 let post_id = data.post_id;
449 let post_view = blocking(context.pool(), move |conn| {
450 PostView::read(conn, post_id, Some(local_user_view.person.id))
454 let res = PostResponse { post_view };
456 context.chat_server().do_send(SendPost {
457 op: UserOperation::EditPost,
466 #[async_trait::async_trait(?Send)]
467 impl Perform for DeletePost {
468 type Response = PostResponse;
472 context: &Data<LemmyContext>,
473 websocket_id: Option<ConnectionId>,
474 ) -> Result<PostResponse, LemmyError> {
475 let data: &DeletePost = &self;
476 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
478 let post_id = data.post_id;
479 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
482 local_user_view.person.id,
483 orig_post.community_id,
488 // Verify that only the creator can delete
489 if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
490 return Err(ApiError::err("no_post_edit_allowed").into());
494 let post_id = data.post_id;
495 let deleted = data.deleted;
496 let updated_post = blocking(context.pool(), move |conn| {
497 Post::update_deleted(conn, post_id, deleted)
504 .send_delete(&local_user_view.person, context)
508 .send_undo_delete(&local_user_view.person, context)
513 let post_id = data.post_id;
514 let post_view = blocking(context.pool(), move |conn| {
515 PostView::read(conn, post_id, Some(local_user_view.person.id))
519 let res = PostResponse { post_view };
521 context.chat_server().do_send(SendPost {
522 op: UserOperation::DeletePost,
531 #[async_trait::async_trait(?Send)]
532 impl Perform for RemovePost {
533 type Response = PostResponse;
537 context: &Data<LemmyContext>,
538 websocket_id: Option<ConnectionId>,
539 ) -> Result<PostResponse, LemmyError> {
540 let data: &RemovePost = &self;
541 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
543 let post_id = data.post_id;
544 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
547 local_user_view.person.id,
548 orig_post.community_id,
553 // Verify that only the mods can remove
556 local_user_view.person.id,
557 orig_post.community_id,
562 let post_id = data.post_id;
563 let removed = data.removed;
564 let updated_post = blocking(context.pool(), move |conn| {
565 Post::update_removed(conn, post_id, removed)
570 let form = ModRemovePostForm {
571 mod_person_id: local_user_view.person.id,
572 post_id: data.post_id,
573 removed: Some(removed),
574 reason: data.reason.to_owned(),
576 blocking(context.pool(), move |conn| {
577 ModRemovePost::create(conn, &form)
584 .send_remove(&local_user_view.person, context)
588 .send_undo_remove(&local_user_view.person, context)
593 let post_id = data.post_id;
594 let person_id = local_user_view.person.id;
595 let post_view = blocking(context.pool(), move |conn| {
596 PostView::read(conn, post_id, Some(person_id))
600 let res = PostResponse { post_view };
602 context.chat_server().do_send(SendPost {
603 op: UserOperation::RemovePost,
612 #[async_trait::async_trait(?Send)]
613 impl Perform for LockPost {
614 type Response = PostResponse;
618 context: &Data<LemmyContext>,
619 websocket_id: Option<ConnectionId>,
620 ) -> Result<PostResponse, LemmyError> {
621 let data: &LockPost = &self;
622 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
624 let post_id = data.post_id;
625 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
628 local_user_view.person.id,
629 orig_post.community_id,
634 // Verify that only the mods can lock
637 local_user_view.person.id,
638 orig_post.community_id,
643 let post_id = data.post_id;
644 let locked = data.locked;
645 let updated_post = blocking(context.pool(), move |conn| {
646 Post::update_locked(conn, post_id, locked)
651 let form = ModLockPostForm {
652 mod_person_id: local_user_view.person.id,
653 post_id: data.post_id,
654 locked: Some(locked),
656 blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
660 .send_update(&local_user_view.person, context)
664 let post_id = data.post_id;
665 let post_view = blocking(context.pool(), move |conn| {
666 PostView::read(conn, post_id, Some(local_user_view.person.id))
670 let res = PostResponse { post_view };
672 context.chat_server().do_send(SendPost {
673 op: UserOperation::LockPost,
682 #[async_trait::async_trait(?Send)]
683 impl Perform for StickyPost {
684 type Response = PostResponse;
688 context: &Data<LemmyContext>,
689 websocket_id: Option<ConnectionId>,
690 ) -> Result<PostResponse, LemmyError> {
691 let data: &StickyPost = &self;
692 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
694 let post_id = data.post_id;
695 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
698 local_user_view.person.id,
699 orig_post.community_id,
704 // Verify that only the mods can sticky
707 local_user_view.person.id,
708 orig_post.community_id,
713 let post_id = data.post_id;
714 let stickied = data.stickied;
715 let updated_post = blocking(context.pool(), move |conn| {
716 Post::update_stickied(conn, post_id, stickied)
721 let form = ModStickyPostForm {
722 mod_person_id: local_user_view.person.id,
723 post_id: data.post_id,
724 stickied: Some(stickied),
726 blocking(context.pool(), move |conn| {
727 ModStickyPost::create(conn, &form)
732 // TODO stickied should pry work like locked for ease of use
734 .send_update(&local_user_view.person, context)
738 let post_id = data.post_id;
739 let post_view = blocking(context.pool(), move |conn| {
740 PostView::read(conn, post_id, Some(local_user_view.person.id))
744 let res = PostResponse { post_view };
746 context.chat_server().do_send(SendPost {
747 op: UserOperation::StickyPost,
756 #[async_trait::async_trait(?Send)]
757 impl Perform for SavePost {
758 type Response = PostResponse;
762 context: &Data<LemmyContext>,
763 _websocket_id: Option<ConnectionId>,
764 ) -> Result<PostResponse, LemmyError> {
765 let data: &SavePost = &self;
766 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
768 let post_saved_form = PostSavedForm {
769 post_id: data.post_id,
770 person_id: local_user_view.person.id,
774 let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
775 if blocking(context.pool(), save).await?.is_err() {
776 return Err(ApiError::err("couldnt_save_post").into());
779 let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
780 if blocking(context.pool(), unsave).await?.is_err() {
781 return Err(ApiError::err("couldnt_save_post").into());
785 let post_id = data.post_id;
786 let person_id = local_user_view.person.id;
787 let post_view = blocking(context.pool(), move |conn| {
788 PostView::read(conn, post_id, Some(person_id))
792 Ok(PostResponse { post_view })
796 /// Creates a post report and notifies the moderators of the community
797 #[async_trait::async_trait(?Send)]
798 impl Perform for CreatePostReport {
799 type Response = CreatePostReportResponse;
803 context: &Data<LemmyContext>,
804 websocket_id: Option<ConnectionId>,
805 ) -> Result<CreatePostReportResponse, LemmyError> {
806 let data: &CreatePostReport = &self;
807 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
809 // check size of report and check for whitespace
810 let reason = data.reason.trim();
811 if reason.is_empty() {
812 return Err(ApiError::err("report_reason_required").into());
814 if reason.chars().count() > 1000 {
815 return Err(ApiError::err("report_too_long").into());
818 let person_id = local_user_view.person.id;
819 let post_id = data.post_id;
820 let post_view = blocking(context.pool(), move |conn| {
821 PostView::read(&conn, post_id, None)
825 check_community_ban(person_id, post_view.community.id, context.pool()).await?;
827 let report_form = PostReportForm {
828 creator_id: person_id,
830 original_post_name: post_view.post.name,
831 original_post_url: post_view.post.url,
832 original_post_body: post_view.post.body,
833 reason: data.reason.to_owned(),
836 let report = match blocking(context.pool(), move |conn| {
837 PostReport::report(conn, &report_form)
841 Ok(report) => report,
842 Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
845 let res = CreatePostReportResponse { success: true };
847 context.chat_server().do_send(SendUserRoomMessage {
848 op: UserOperation::CreatePostReport,
849 response: res.clone(),
850 local_recipient_id: local_user_view.local_user.id,
854 context.chat_server().do_send(SendModRoomMessage {
855 op: UserOperation::CreatePostReport,
857 community_id: post_view.community.id,
865 /// Resolves or unresolves a post report and notifies the moderators of the community
866 #[async_trait::async_trait(?Send)]
867 impl Perform for ResolvePostReport {
868 type Response = ResolvePostReportResponse;
872 context: &Data<LemmyContext>,
873 websocket_id: Option<ConnectionId>,
874 ) -> Result<ResolvePostReportResponse, LemmyError> {
875 let data: &ResolvePostReport = &self;
876 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
878 let report_id = data.report_id;
879 let report = blocking(context.pool(), move |conn| {
880 PostReportView::read(&conn, report_id)
884 let person_id = local_user_view.person.id;
885 is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
887 let resolved = data.resolved;
888 let resolve_fun = move |conn: &'_ _| {
890 PostReport::resolve(conn, report_id, person_id)
892 PostReport::unresolve(conn, report_id, person_id)
896 let res = ResolvePostReportResponse {
901 if blocking(context.pool(), resolve_fun).await?.is_err() {
902 return Err(ApiError::err("couldnt_resolve_report").into());
905 context.chat_server().do_send(SendModRoomMessage {
906 op: UserOperation::ResolvePostReport,
907 response: res.clone(),
908 community_id: report.community.id,
916 /// Lists post reports for a community if an id is supplied
917 /// or returns all post reports for communities a user moderates
918 #[async_trait::async_trait(?Send)]
919 impl Perform for ListPostReports {
920 type Response = ListPostReportsResponse;
924 context: &Data<LemmyContext>,
925 websocket_id: Option<ConnectionId>,
926 ) -> Result<ListPostReportsResponse, LemmyError> {
927 let data: &ListPostReports = &self;
928 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
930 let person_id = local_user_view.person.id;
931 let community_id = data.community;
933 collect_moderated_communities(person_id, community_id, context.pool()).await?;
935 let page = data.page;
936 let limit = data.limit;
937 let posts = blocking(context.pool(), move |conn| {
938 PostReportQueryBuilder::create(conn)
939 .community_ids(community_ids)
946 let res = ListPostReportsResponse { posts };
948 context.chat_server().do_send(SendUserRoomMessage {
949 op: UserOperation::ListPostReports,
950 response: res.clone(),
951 local_recipient_id: local_user_view.local_user.id,