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,
86 embed_title: iframely_title,
87 embed_description: iframely_description,
88 embed_html: iframely_html,
89 thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
94 match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
97 let err_type = if e.to_string() == "value too long for type character varying(200)" {
100 "couldnt_create_post"
103 return Err(ApiError::err(err_type).into());
107 let inserted_post_id = inserted_post.id;
108 let updated_post = match blocking(context.pool(), move |conn| -> Result<Post, LemmyError> {
109 let apub_id = generate_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string())?;
110 Ok(Post::update_ap_id(conn, inserted_post_id, apub_id)?)
115 Err(_e) => return Err(ApiError::err("couldnt_create_post").into()),
119 .send_create(&local_user_view.person, context)
122 // They like their own post by default
123 let like_form = PostLikeForm {
124 post_id: inserted_post.id,
125 person_id: local_user_view.person.id,
129 let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
130 if blocking(context.pool(), like).await?.is_err() {
131 return Err(ApiError::err("couldnt_like_post").into());
135 .send_like(&local_user_view.person, context)
139 let inserted_post_id = inserted_post.id;
140 let post_view = match blocking(context.pool(), move |conn| {
141 PostView::read(conn, inserted_post_id, Some(local_user_view.person.id))
146 Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
149 let res = PostResponse { post_view };
151 context.chat_server().do_send(SendPost {
152 op: UserOperation::CreatePost,
161 #[async_trait::async_trait(?Send)]
162 impl Perform for GetPost {
163 type Response = GetPostResponse;
167 context: &Data<LemmyContext>,
168 _websocket_id: Option<ConnectionId>,
169 ) -> Result<GetPostResponse, LemmyError> {
170 let data: &GetPost = &self;
171 let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
172 let person_id = local_user_view.map(|u| u.person.id);
175 let post_view = match blocking(context.pool(), move |conn| {
176 PostView::read(conn, id, person_id)
181 Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
185 let comments = blocking(context.pool(), move |conn| {
186 CommentQueryBuilder::create(conn)
187 .my_person_id(person_id)
194 let community_id = post_view.community.id;
195 let moderators = blocking(context.pool(), move |conn| {
196 CommunityModeratorView::for_community(conn, community_id)
200 // Necessary for the sidebar
201 let community_view = match blocking(context.pool(), move |conn| {
202 CommunityView::read(conn, community_id, person_id)
206 Ok(community) => community,
207 Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
212 .send(GetPostUsersOnline { post_id: data.id })
227 #[async_trait::async_trait(?Send)]
228 impl Perform for GetPosts {
229 type Response = GetPostsResponse;
233 context: &Data<LemmyContext>,
234 _websocket_id: Option<ConnectionId>,
235 ) -> Result<GetPostsResponse, LemmyError> {
236 let data: &GetPosts = &self;
237 let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
239 let person_id = match &local_user_view {
240 Some(uv) => Some(uv.person.id),
244 let show_nsfw = match &local_user_view {
245 Some(uv) => uv.local_user.show_nsfw,
249 let type_ = ListingType::from_str(&data.type_)?;
250 let sort = SortType::from_str(&data.sort)?;
252 let page = data.page;
253 let limit = data.limit;
254 let community_id = data.community_id;
255 let community_name = data.community_name.to_owned();
256 let saved_only = data.saved_only;
258 let posts = match blocking(context.pool(), move |conn| {
259 PostQueryBuilder::create(conn)
260 .listing_type(&type_)
262 .show_nsfw(show_nsfw)
263 .community_id(community_id)
264 .community_name(community_name)
265 .saved_only(saved_only)
266 .my_person_id(person_id)
274 Err(_e) => return Err(ApiError::err("couldnt_get_posts").into()),
277 Ok(GetPostsResponse { posts })
281 #[async_trait::async_trait(?Send)]
282 impl Perform for CreatePostLike {
283 type Response = PostResponse;
287 context: &Data<LemmyContext>,
288 websocket_id: Option<ConnectionId>,
289 ) -> Result<PostResponse, LemmyError> {
290 let data: &CreatePostLike = &self;
291 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
293 // Don't do a downvote if site has downvotes disabled
294 check_downvotes_enabled(data.score, context.pool()).await?;
296 // Check for a community ban
297 let post_id = data.post_id;
298 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
300 check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
302 let like_form = PostLikeForm {
303 post_id: data.post_id,
304 person_id: local_user_view.person.id,
308 // Remove any likes first
309 let person_id = local_user_view.person.id;
310 blocking(context.pool(), move |conn| {
311 PostLike::remove(conn, person_id, post_id)
315 // Only add the like if the score isnt 0
316 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
318 let like_form2 = like_form.clone();
319 let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
320 if blocking(context.pool(), like).await?.is_err() {
321 return Err(ApiError::err("couldnt_like_post").into());
324 if like_form.score == 1 {
325 post.send_like(&local_user_view.person, context).await?;
326 } else if like_form.score == -1 {
327 post.send_dislike(&local_user_view.person, context).await?;
331 .send_undo_like(&local_user_view.person, context)
335 let post_id = data.post_id;
336 let person_id = local_user_view.person.id;
337 let post_view = match blocking(context.pool(), move |conn| {
338 PostView::read(conn, post_id, Some(person_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 local_user_view = get_local_user_view_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??;
381 local_user_view.person.id,
382 orig_post.community_id,
387 // Verify that only the creator can edit
388 if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
389 return Err(ApiError::err("no_post_edit_allowed").into());
392 // Fetch Iframely and Pictrs cached image
393 let data_url = data.url.as_ref();
394 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
395 fetch_iframely_and_pictrs_data(context.client(), data_url).await;
397 let post_form = PostForm {
398 creator_id: orig_post.creator_id.to_owned(),
399 community_id: orig_post.community_id,
400 name: data.name.trim().to_owned(),
401 url: data_url.map(|u| u.to_owned().into()),
402 body: data.body.to_owned(),
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.map(|u| u.into()),
409 ..PostForm::default()
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());
432 .send_update(&local_user_view.person, context)
435 let post_id = data.post_id;
436 let post_view = blocking(context.pool(), move |conn| {
437 PostView::read(conn, post_id, Some(local_user_view.person.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 local_user_view = get_local_user_view_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??;
469 local_user_view.person.id,
470 orig_post.community_id,
475 // Verify that only the creator can delete
476 if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
477 return Err(ApiError::err("no_post_edit_allowed").into());
481 let post_id = data.post_id;
482 let deleted = data.deleted;
483 let updated_post = blocking(context.pool(), move |conn| {
484 Post::update_deleted(conn, post_id, deleted)
491 .send_delete(&local_user_view.person, context)
495 .send_undo_delete(&local_user_view.person, context)
500 let post_id = data.post_id;
501 let post_view = blocking(context.pool(), move |conn| {
502 PostView::read(conn, post_id, Some(local_user_view.person.id))
506 let res = PostResponse { post_view };
508 context.chat_server().do_send(SendPost {
509 op: UserOperation::DeletePost,
518 #[async_trait::async_trait(?Send)]
519 impl Perform for RemovePost {
520 type Response = PostResponse;
524 context: &Data<LemmyContext>,
525 websocket_id: Option<ConnectionId>,
526 ) -> Result<PostResponse, LemmyError> {
527 let data: &RemovePost = &self;
528 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
530 let post_id = data.post_id;
531 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
534 local_user_view.person.id,
535 orig_post.community_id,
540 // Verify that only the mods can remove
543 local_user_view.person.id,
544 orig_post.community_id,
549 let post_id = data.post_id;
550 let removed = data.removed;
551 let updated_post = blocking(context.pool(), move |conn| {
552 Post::update_removed(conn, post_id, removed)
557 let form = ModRemovePostForm {
558 mod_person_id: local_user_view.person.id,
559 post_id: data.post_id,
560 removed: Some(removed),
561 reason: data.reason.to_owned(),
563 blocking(context.pool(), move |conn| {
564 ModRemovePost::create(conn, &form)
571 .send_remove(&local_user_view.person, context)
575 .send_undo_remove(&local_user_view.person, context)
580 let post_id = data.post_id;
581 let person_id = local_user_view.person.id;
582 let post_view = blocking(context.pool(), move |conn| {
583 PostView::read(conn, post_id, Some(person_id))
587 let res = PostResponse { post_view };
589 context.chat_server().do_send(SendPost {
590 op: UserOperation::RemovePost,
599 #[async_trait::async_trait(?Send)]
600 impl Perform for LockPost {
601 type Response = PostResponse;
605 context: &Data<LemmyContext>,
606 websocket_id: Option<ConnectionId>,
607 ) -> Result<PostResponse, LemmyError> {
608 let data: &LockPost = &self;
609 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
611 let post_id = data.post_id;
612 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
615 local_user_view.person.id,
616 orig_post.community_id,
621 // Verify that only the mods can lock
624 local_user_view.person.id,
625 orig_post.community_id,
630 let post_id = data.post_id;
631 let locked = data.locked;
632 let updated_post = blocking(context.pool(), move |conn| {
633 Post::update_locked(conn, post_id, locked)
638 let form = ModLockPostForm {
639 mod_person_id: local_user_view.person.id,
640 post_id: data.post_id,
641 locked: Some(locked),
643 blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
647 .send_update(&local_user_view.person, context)
651 let post_id = data.post_id;
652 let post_view = blocking(context.pool(), move |conn| {
653 PostView::read(conn, post_id, Some(local_user_view.person.id))
657 let res = PostResponse { post_view };
659 context.chat_server().do_send(SendPost {
660 op: UserOperation::LockPost,
669 #[async_trait::async_trait(?Send)]
670 impl Perform for StickyPost {
671 type Response = PostResponse;
675 context: &Data<LemmyContext>,
676 websocket_id: Option<ConnectionId>,
677 ) -> Result<PostResponse, LemmyError> {
678 let data: &StickyPost = &self;
679 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
681 let post_id = data.post_id;
682 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
685 local_user_view.person.id,
686 orig_post.community_id,
691 // Verify that only the mods can sticky
694 local_user_view.person.id,
695 orig_post.community_id,
700 let post_id = data.post_id;
701 let stickied = data.stickied;
702 let updated_post = blocking(context.pool(), move |conn| {
703 Post::update_stickied(conn, post_id, stickied)
708 let form = ModStickyPostForm {
709 mod_person_id: local_user_view.person.id,
710 post_id: data.post_id,
711 stickied: Some(stickied),
713 blocking(context.pool(), move |conn| {
714 ModStickyPost::create(conn, &form)
719 // TODO stickied should pry work like locked for ease of use
721 .send_update(&local_user_view.person, context)
725 let post_id = data.post_id;
726 let post_view = blocking(context.pool(), move |conn| {
727 PostView::read(conn, post_id, Some(local_user_view.person.id))
731 let res = PostResponse { post_view };
733 context.chat_server().do_send(SendPost {
734 op: UserOperation::StickyPost,
743 #[async_trait::async_trait(?Send)]
744 impl Perform for SavePost {
745 type Response = PostResponse;
749 context: &Data<LemmyContext>,
750 _websocket_id: Option<ConnectionId>,
751 ) -> Result<PostResponse, LemmyError> {
752 let data: &SavePost = &self;
753 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
755 let post_saved_form = PostSavedForm {
756 post_id: data.post_id,
757 person_id: local_user_view.person.id,
761 let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
762 if blocking(context.pool(), save).await?.is_err() {
763 return Err(ApiError::err("couldnt_save_post").into());
766 let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
767 if blocking(context.pool(), unsave).await?.is_err() {
768 return Err(ApiError::err("couldnt_save_post").into());
772 let post_id = data.post_id;
773 let person_id = local_user_view.person.id;
774 let post_view = blocking(context.pool(), move |conn| {
775 PostView::read(conn, post_id, Some(person_id))
779 Ok(PostResponse { post_view })
783 /// Creates a post report and notifies the moderators of the community
784 #[async_trait::async_trait(?Send)]
785 impl Perform for CreatePostReport {
786 type Response = CreatePostReportResponse;
790 context: &Data<LemmyContext>,
791 websocket_id: Option<ConnectionId>,
792 ) -> Result<CreatePostReportResponse, LemmyError> {
793 let data: &CreatePostReport = &self;
794 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
796 // check size of report and check for whitespace
797 let reason = data.reason.trim();
798 if reason.is_empty() {
799 return Err(ApiError::err("report_reason_required").into());
801 if reason.chars().count() > 1000 {
802 return Err(ApiError::err("report_too_long").into());
805 let person_id = local_user_view.person.id;
806 let post_id = data.post_id;
807 let post_view = blocking(context.pool(), move |conn| {
808 PostView::read(&conn, post_id, None)
812 check_community_ban(person_id, post_view.community.id, context.pool()).await?;
814 let report_form = PostReportForm {
815 creator_id: person_id,
817 original_post_name: post_view.post.name,
818 original_post_url: post_view.post.url,
819 original_post_body: post_view.post.body,
820 reason: data.reason.to_owned(),
823 let report = match blocking(context.pool(), move |conn| {
824 PostReport::report(conn, &report_form)
828 Ok(report) => report,
829 Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
832 let res = CreatePostReportResponse { success: true };
834 context.chat_server().do_send(SendUserRoomMessage {
835 op: UserOperation::CreatePostReport,
836 response: res.clone(),
837 local_recipient_id: local_user_view.local_user.id,
841 context.chat_server().do_send(SendModRoomMessage {
842 op: UserOperation::CreatePostReport,
844 community_id: post_view.community.id,
852 /// Resolves or unresolves a post report and notifies the moderators of the community
853 #[async_trait::async_trait(?Send)]
854 impl Perform for ResolvePostReport {
855 type Response = ResolvePostReportResponse;
859 context: &Data<LemmyContext>,
860 websocket_id: Option<ConnectionId>,
861 ) -> Result<ResolvePostReportResponse, LemmyError> {
862 let data: &ResolvePostReport = &self;
863 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
865 let report_id = data.report_id;
866 let report = blocking(context.pool(), move |conn| {
867 PostReportView::read(&conn, report_id)
871 let person_id = local_user_view.person.id;
872 is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
874 let resolved = data.resolved;
875 let resolve_fun = move |conn: &'_ _| {
877 PostReport::resolve(conn, report_id, person_id)
879 PostReport::unresolve(conn, report_id, person_id)
883 let res = ResolvePostReportResponse {
888 if blocking(context.pool(), resolve_fun).await?.is_err() {
889 return Err(ApiError::err("couldnt_resolve_report").into());
892 context.chat_server().do_send(SendModRoomMessage {
893 op: UserOperation::ResolvePostReport,
894 response: res.clone(),
895 community_id: report.community.id,
903 /// Lists post reports for a community if an id is supplied
904 /// or returns all post reports for communities a user moderates
905 #[async_trait::async_trait(?Send)]
906 impl Perform for ListPostReports {
907 type Response = ListPostReportsResponse;
911 context: &Data<LemmyContext>,
912 websocket_id: Option<ConnectionId>,
913 ) -> Result<ListPostReportsResponse, LemmyError> {
914 let data: &ListPostReports = &self;
915 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
917 let person_id = local_user_view.person.id;
918 let community_id = data.community;
920 collect_moderated_communities(person_id, community_id, context.pool()).await?;
922 let page = data.page;
923 let limit = data.limit;
924 let posts = blocking(context.pool(), move |conn| {
925 PostReportQueryBuilder::create(conn)
926 .community_ids(community_ids)
933 let res = ListPostReportsResponse { posts };
935 context.chat_server().do_send(SendUserRoomMessage {
936 op: UserOperation::ListPostReports,
937 response: res.clone(),
938 local_recipient_id: local_user_view.local_user.id,