4 collect_moderated_communities,
10 use actix_web::web::Data;
11 use lemmy_apub::{ApubLikeableType, ApubObjectType};
15 comment_view::CommentQueryBuilder,
16 community::{community_moderator_view::CommunityModeratorView, community_view::CommunityView},
17 post_report_view::{PostReportQueryBuilder, PostReportView},
18 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 community = blocking(context.pool(), move |conn| {
200 CommunityView::read(conn, community_id, user_id)
204 let community_id = post_view.community.id;
205 let moderators = blocking(context.pool(), move |conn| {
206 CommunityModeratorView::for_community(conn, community_id)
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 user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
239 let user_id = match &user {
240 Some(user) => Some(user.id),
244 let show_nsfw = match &user {
245 Some(user) => 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 posts = match blocking(context.pool(), move |conn| {
257 PostQueryBuilder::create(conn)
258 .listing_type(&type_)
260 .show_nsfw(show_nsfw)
261 .community_id(community_id)
262 .community_name(community_name)
271 Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
274 Ok(GetPostsResponse { posts })
278 #[async_trait::async_trait(?Send)]
279 impl Perform for CreatePostLike {
280 type Response = PostResponse;
284 context: &Data<LemmyContext>,
285 websocket_id: Option<ConnectionId>,
286 ) -> Result<PostResponse, LemmyError> {
287 let data: &CreatePostLike = &self;
288 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
290 // Don't do a downvote if site has downvotes disabled
291 if data.score == -1 {
292 let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
293 if !site_view.site.enable_downvotes {
294 return Err(APIError::err("downvotes_disabled").into());
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 edit_id = data.edit_id;
378 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_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 edit_id = data.edit_id;
413 let res = blocking(context.pool(), move |conn| {
414 Post::update(conn, edit_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 edit_id = data.edit_id;
434 let post_view = blocking(context.pool(), move |conn| {
435 PostView::read(conn, edit_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 edit_id = data.edit_id;
464 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_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 edit_id = data.edit_id;
475 let deleted = data.deleted;
476 let updated_post = blocking(context.pool(), move |conn| {
477 Post::update_deleted(conn, edit_id, deleted)
483 updated_post.send_delete(&user, context).await?;
485 updated_post.send_undo_delete(&user, context).await?;
489 let edit_id = data.edit_id;
490 let post_view = blocking(context.pool(), move |conn| {
491 PostView::read(conn, edit_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 edit_id = data.edit_id;
520 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_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 edit_id = data.edit_id;
529 let removed = data.removed;
530 let updated_post = blocking(context.pool(), move |conn| {
531 Post::update_removed(conn, edit_id, removed)
536 let form = ModRemovePostForm {
537 mod_user_id: user.id,
538 post_id: data.edit_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 edit_id = data.edit_id;
556 let user_id = user.id;
557 let post_view = blocking(context.pool(), move |conn| {
558 PostView::read(conn, edit_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 edit_id = data.edit_id;
587 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_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 edit_id = data.edit_id;
596 let locked = data.locked;
597 let updated_post = blocking(context.pool(), move |conn| {
598 Post::update_locked(conn, edit_id, locked)
603 let form = ModLockPostForm {
604 mod_user_id: user.id,
605 post_id: data.edit_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 edit_id = data.edit_id;
615 let post_view = blocking(context.pool(), move |conn| {
616 PostView::read(conn, edit_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 edit_id = data.edit_id;
645 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_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 edit_id = data.edit_id;
654 let stickied = data.stickied;
655 let updated_post = blocking(context.pool(), move |conn| {
656 Post::update_stickied(conn, edit_id, stickied)
661 let form = ModStickyPostForm {
662 mod_user_id: user.id,
663 post_id: data.edit_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 edit_id = data.edit_id;
677 let post_view = blocking(context.pool(), move |conn| {
678 PostView::read(conn, edit_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 #[async_trait::async_trait(?Send)]
735 impl Perform for PostJoin {
736 type Response = PostJoinResponse;
740 context: &Data<LemmyContext>,
741 websocket_id: Option<ConnectionId>,
742 ) -> Result<PostJoinResponse, LemmyError> {
743 let data: &PostJoin = &self;
745 if let Some(ws_id) = websocket_id {
746 context.chat_server().do_send(JoinPostRoom {
747 post_id: data.post_id,
752 Ok(PostJoinResponse { joined: true })
756 /// Creates a post report and notifies the moderators of the community
757 #[async_trait::async_trait(?Send)]
758 impl Perform for CreatePostReport {
759 type Response = CreatePostReportResponse;
763 context: &Data<LemmyContext>,
764 websocket_id: Option<ConnectionId>,
765 ) -> Result<CreatePostReportResponse, LemmyError> {
766 let data: &CreatePostReport = &self;
767 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
769 // check size of report and check for whitespace
770 let reason = data.reason.trim();
771 if reason.is_empty() {
772 return Err(APIError::err("report_reason_required").into());
774 if reason.len() > 1000 {
775 return Err(APIError::err("report_too_long").into());
778 let user_id = user.id;
779 let post_id = data.post_id;
780 let post_view = blocking(context.pool(), move |conn| {
781 PostView::read(&conn, post_id, None)
785 check_community_ban(user_id, post_view.community.id, context.pool()).await?;
787 let report_form = PostReportForm {
790 original_post_name: post_view.post.name,
791 original_post_url: post_view.post.url,
792 original_post_body: post_view.post.body,
793 reason: data.reason.to_owned(),
796 let report = match blocking(context.pool(), move |conn| {
797 PostReport::report(conn, &report_form)
801 Ok(report) => report,
802 Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
805 let res = CreatePostReportResponse { success: true };
807 context.chat_server().do_send(SendUserRoomMessage {
808 op: UserOperation::CreatePostReport,
809 response: res.clone(),
810 recipient_id: user.id,
814 context.chat_server().do_send(SendModRoomMessage {
815 op: UserOperation::CreatePostReport,
817 community_id: post_view.community.id,
825 /// Resolves or unresolves a post report and notifies the moderators of the community
826 #[async_trait::async_trait(?Send)]
827 impl Perform for ResolvePostReport {
828 type Response = ResolvePostReportResponse;
832 context: &Data<LemmyContext>,
833 websocket_id: Option<ConnectionId>,
834 ) -> Result<ResolvePostReportResponse, LemmyError> {
835 let data: &ResolvePostReport = &self;
836 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
838 let report_id = data.report_id;
839 let report = blocking(context.pool(), move |conn| {
840 PostReportView::read(&conn, report_id)
844 let user_id = user.id;
845 is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
847 let resolved = data.resolved;
848 let resolve_fun = move |conn: &'_ _| {
850 PostReport::resolve(conn, report_id, user_id)
852 PostReport::unresolve(conn, report_id, user_id)
856 let res = ResolvePostReportResponse {
861 if blocking(context.pool(), resolve_fun).await?.is_err() {
862 return Err(APIError::err("couldnt_resolve_report").into());
865 context.chat_server().do_send(SendModRoomMessage {
866 op: UserOperation::ResolvePostReport,
867 response: res.clone(),
868 community_id: report.community.id,
876 /// Lists post reports for a community if an id is supplied
877 /// or returns all post reports for communities a user moderates
878 #[async_trait::async_trait(?Send)]
879 impl Perform for ListPostReports {
880 type Response = ListPostReportsResponse;
884 context: &Data<LemmyContext>,
885 websocket_id: Option<ConnectionId>,
886 ) -> Result<ListPostReportsResponse, LemmyError> {
887 let data: &ListPostReports = &self;
888 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
890 let user_id = user.id;
891 let community_id = data.community;
893 collect_moderated_communities(user_id, community_id, context.pool()).await?;
895 let page = data.page;
896 let limit = data.limit;
897 let posts = blocking(context.pool(), move |conn| {
898 PostReportQueryBuilder::create(conn)
899 .community_ids(community_ids)
906 let res = ListPostReportsResponse { posts };
908 context.chat_server().do_send(SendUserRoomMessage {
909 op: UserOperation::ListPostReports,
910 response: res.clone(),
911 recipient_id: user.id,